在寫程式時,我們經常會想要拓展一些東西。
例如我們有一個user object,他有自己的屬性跟函數,我們希望將admin與guest基於user稍作修改,重用user中的內容,並不是複製,只是在user上建構一個新的對象。
原型繼承(Prototypal inheritance)這個語言特性能幫助我們實現這個需求。
在JavaScript中,對象有一個特殊的隱藏屬性,不是null就是對另一個Object的引用,該對像稱為原型。
當我們從object讀取一個不存在的屬性時,JavaScript會自動從原型中找尋獲取該屬性,這就是原型繼承。[[prototype]]是隱藏的,但有另一個屬性可以獲取到原型 :
let animal = {
eats: true
};
let cat = {
jumps: true
};
cat.__proto__ = animal; // 設置 rabbit.[[Prototype]] = animal
// 現在這兩個属性我们都能在 cat 中找到:
console.log( cat.eats ); // true
console.log( cat.jumps ); // true
如果animal中有個函數,他一樣可以在cat中取用 :
let animal = {
eats: true,
walk() {
alert("Animal walk");
}
};
let cat = {
jumps: true,
__proto__: animal
};
// walk 方法是从原型中獲得/繼承的
rabbit.walk(); // Animal walk
原型鏈可以很長 :
let animal = {
eats: true,
walk() {
alert("Animal walk");
}
};
let cat = {
jumps: true,
__proto__: animal
};
let foldEar = {
earLength: 2,
__proto__: cat
};
// walk 一樣透過原型鍊取得
longEar.walk(); // Animal walk
alert(foldEar.jumps); // true (繼承cat)
這裡有兩個限制
當然只能有一個[[prototype]]。
__proto__是[[prototype]]的歷史原因而留下來的getter/setter
這兩個是不一樣的,__prototype__是[[prototype]]的getter/setter。
__proto__屬性有點過時了,他的出現是因為歷史原因。現代編程建議我們使用函數Object.getPrototypeof/Object.setPrototypeOf來取代__prototype__去get/set原型。
原型只用於讀取屬性,如果要寫入或刪除屬性直接在object中操作。
在下面的例子我們將cat分配自己的walk :
let animal = {
eats: true,
walk() {
alert("Animal walk");
}
};
let cat = {
__proto__: animal
};
cat.walk = function() {
console.log("cat walk!");
};
cat.walk(); // cat walk!
訪問器(accessor)是一個例外,因為assignment操作是由setter函數處裡的。因此,寫入此類型屬性時實際上跟調用函數相同。
參考以下代碼 :
let user = {
name: "John",
surname: "Smith",
set fullName(value) {
[this.name, this.surname] = value.split(" ");
},
get fullName() {
return `${this.name} ${this.surname}`;
}
};
let admin = {
__proto__: user,
isAdmin: true
};
alert(admin.fullName); // John Smith
// setter 作用
admin.fullName = "Alice Cooper"; // 在admin object中新增name與surname
alert(admin.fullName); // Alice Cooper,admin 的内容被修改了
alert(user.fullName); // John Smith,user 的内容被保護了
for..in迴圈也會把繼承的屬性算入 :
let animal = {
eats: true
};
let cat = {
jumps: true,
__proto__: animal
};
// Object.keys 只返回自己的屬性
console.log(Object.keys(cat)); // jumps
// for..in 會遍歷自己以及繼承的屬性
for(let prop in cat) console.log(prop); // jumps,eats
如果只是要object本身的屬性,並不想要繼承的屬性,那可以使用內建方法obj.hasOwnProperty(key) :
let animal = {
eats: true
};
let cat = {
jumps: true,
__proto__: animal
};
for(let prop in cat) {
let isOwn = cat.hasOwnProperty(prop);
if (isOwn) {
console.log(`Our: ${prop}`); // Our: jumps
} else {
console.log(`Inherited: ${prop}`); // Inherited: eats
}
}
cat從animal中繼承,animal從Object.prototype中繼承(默認繼承),然後像上是null。
那cat.hasOwnProperty方法從哪來的? 圖中可以看到是Object.prototype.hasOwnProperty提供的,換句話說是繼承的,那為什麼for..in迴圈並沒有像eat和jumps那樣出現在迴圈中呢?
Ans: 因為它是不可枚舉的。就像object.prototype的其他屬性,hasOwnProperty有enumerable:false標誌。並且for...in只會列出可枚舉的屬性。